view.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. 'use client';
  2. import { useEffect, useRef, useState } from 'react';
  3. import { DonationAlertData, DonationAlertConfig } from '@/types/donation';
  4. import './style.scss';
  5. type Props = {
  6. alert: DonationAlertData;
  7. config: DonationAlertConfig;
  8. isAudioOnly: boolean;
  9. isVideoOnly: boolean;
  10. onComplete: () => void;
  11. };
  12. /**
  13. * config.message 템플릿의 {이름}, {금액}을 실제 값으로 치환하여 JSX 반환.
  14. * 각 변수에 sender-name / sender-amount 클래스를 적용해 폰트 스타일 유지.
  15. */
  16. function resolveTemplate(template: string, sendName: string, amount: number): React.ReactNode {
  17. const formattedAmount = `${amount.toLocaleString()}원`;
  18. const parts = template.split(/(\{이름\}|\{금액\})/g);
  19. return parts.map((part, i) => {
  20. if (part === '{이름}') {
  21. return <span key={i} className="donation-alert__name">{sendName}</span>;
  22. }
  23. if (part === '{금액}') {
  24. return <span key={i} className="donation-alert__amount">{formattedAmount}</span>;
  25. }
  26. return <span key={i} className="donation-alert__template-text">{part}</span>;
  27. });
  28. }
  29. export default function View({ alert, config, isAudioOnly, isVideoOnly, onComplete }: Props)
  30. {
  31. const [phase, setPhase] = useState<'delay'|'enter'|'show'|'exit'>('delay');
  32. const audioRef = useRef<HTMLAudioElement|null>(null);
  33. const timerRef = useRef<NodeJS.Timeout|null>(null);
  34. useEffect(() => {
  35. const delayMs = (config.playDelaySec || 0) * 1000;
  36. timerRef.current = setTimeout(() => {
  37. setPhase('enter');
  38. // 사운드 재생
  39. if (config.enableSound && config.soundUrl && !isVideoOnly) {
  40. const audio = new Audio(config.soundUrl);
  41. audioRef.current = audio;
  42. audio.play().catch(() => {});
  43. }
  44. // enter → show (Animate.css 기본 1초)
  45. setTimeout(() => {
  46. setPhase('show');
  47. timerRef.current = setTimeout(() => {
  48. setPhase('exit');
  49. setTimeout(() => {
  50. onComplete();
  51. }, 500);
  52. }, (config.displayDurationSec || 10) * 1000);
  53. }, 1000);
  54. }, delayMs);
  55. return () => {
  56. if (timerRef.current) {
  57. clearTimeout(timerRef.current);
  58. }
  59. if (audioRef.current) {
  60. audioRef.current.pause();
  61. audioRef.current = null;
  62. }
  63. };
  64. }, []);
  65. if (phase === 'delay') {
  66. return null;
  67. }
  68. // Animate.css 클래스
  69. const popupAnim = config.popupEffect || 'fadeIn';
  70. const textAnim = config.textEffect ? `animate__animated animate__infinite animate__${config.textEffect}` : '';
  71. const containerClass = phase === 'enter'
  72. ? `donation-alert animate__animated animate__${popupAnim}`
  73. : phase === 'exit'
  74. ? 'donation-alert animate__animated animate__fadeOut'
  75. : 'donation-alert alert-show';
  76. // CSS custom properties
  77. const cssVars = {
  78. '--nickname-font-family': config.nicknameFontFamily || 'inherit',
  79. '--nickname-font-size': `${config.nicknameFontSize}px`,
  80. '--nickname-font-color': config.nicknameFontColor || '#FFD700',
  81. '--amount-font-family': config.amountFontFamily || 'inherit',
  82. '--amount-font-size': `${config.amountFontSize}px`,
  83. '--amount-font-color': config.amountFontColor || '#FF6B35',
  84. '--message-font-family': config.messageFontFamily || 'inherit',
  85. '--message-font-size': `${config.messageFontSize}px`,
  86. '--message-font-color': config.messageFontColor || '#FFFFFF',
  87. '--template-font-family': config.templateFontFamily || 'inherit',
  88. '--template-font-size': `${config.templateFontSize}px`,
  89. '--template-font-color': config.templateFontColor || '#FFFFFF',
  90. } as React.CSSProperties;
  91. return (
  92. <div className={containerClass} style={cssVars}>
  93. {/* 이미지 */}
  94. {!isAudioOnly && config.enableImage && config.imageUrl && (
  95. <div className="donation-alert__image">
  96. <img src={config.imageUrl} alt="donation" />
  97. </div>
  98. )}
  99. {/* 후원 정보 */}
  100. {!isAudioOnly && (
  101. <div className="donation-alert__content">
  102. {/* 알림 메시지 (config.message 템플릿 치환) */}
  103. <div className={`donation-alert__sender ${textAnim}`}>
  104. {config.message
  105. ? resolveTemplate(config.message, alert.sendName, alert.amount)
  106. : (<>
  107. <span className="donation-alert__name">{alert.sendName}</span>
  108. <span className="donation-alert__amount">{alert.amount.toLocaleString()}원</span>
  109. </>)
  110. }
  111. </div>
  112. {/* 후원자 전달 내용 */}
  113. {alert.message && (
  114. <div className={`donation-alert__message ${textAnim}`}>{alert.message}</div>
  115. )}
  116. </div>
  117. )}
  118. </div>
  119. );
  120. }